package edu.neu.swc.skynet.filter;

import com.alibaba.fastjson.JSON;
import edu.neu.swc.skynet.constant.CommonConstant;
import edu.neu.swc.skynet.constant.GatewayConstant;
import edu.neu.swc.skynet.util.TokenParseUtil;
import edu.neu.swc.skynet.vo.JwtToken;
import edu.neu.swc.skynet.vo.LoginUserInfo;
import edu.neu.swc.skynet.vo.UsernameAndPassword;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;

/**
 * <h1>全局登录鉴权</h1>
 */
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {
    /** 注册中心客户端，可以从注册中心获取微服务实例信息 */
    private final LoadBalancerClient loadBalancerClient;


    private final RestTemplate restTemplate;

    public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate) {
        this.loadBalancerClient = loadBalancerClient;
        this.restTemplate = restTemplate;
    }

    /**
     * <h2>登录，注册，鉴权</h2>
     * 1，如果是登录注册，则去授权中心拿到Token，并返回给客户端
     * 2，如果是访问其他服务，则鉴权，没有权限则返回401
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //1.如果是登录
        if(request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)){
            //去授权中心获取token
            String token = getTokenFromAuthorityCenter(
                    request,GatewayConstant.AUTHORITY_CENTER_TOKEN_URI_FORMAT
            );
            //header中不能设置null
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token? "null" : token
            );
            //header中需要添加请求头的访问权限
            response.getHeaders().add(
                    "Access-Control-Expose-Headers",
                    CommonConstant.JWT_USER_INFO_KEY
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        //2,如果是注册
        if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)){
            //去授权中心拿token:先创建用户，再返回token
            String token = getTokenFromAuthorityCenter(
                    request,
                    GatewayConstant.AUTHORITY_CENTER_REGISTER_URI_FORMAT
            );
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token? "null" : token
            );

            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        //3.去访问其他的服务，则鉴权校验是否从 Token 中解析中用户信息
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
        LoginUserInfo loginUserInfo = null;

        try{
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        } catch(Exception ex){
            log.error("parse user info from token error:[{}]", ex.getMessage(),ex);
        }

        //如果获取不到用户登录信息
        if(null == loginUserInfo){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //如果存在，则放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }

    /**
     * <h2>从授权中心获取token</h2>
     */
    private String getTokenFromAuthorityCenter(ServerHttpRequest request,String uriFormat){

        //service id就是服务名字，负载均衡
        ServiceInstance serviceInstance = loadBalancerClient.choose(
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID
        );
        log.info("Nacos client info:[{}],[{}],[{}]",
                serviceInstance.getServiceId(),
                serviceInstance.getInstanceId(),
                JSON.toJSONString(serviceInstance.getMetadata()));

        String requestUrl = String.format(
                uriFormat,
                serviceInstance.getHost(),
                serviceInstance.getPort()
        );

        UsernameAndPassword requestBody = JSON.parseObject(
                parseBodyFromRequest(request),
                UsernameAndPassword.class
        );

        log.info("login request url and body:[{}],[{}]",
                requestUrl,JSON.toJSONString(requestBody));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        JwtToken token = restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(requestBody), headers),
                JwtToken.class
        );

        if(null != token){
            return token.getToken();
        }

        //解析通过，则放行
        return null;
    }

    /**
     * <h2>从post请求中获取请求数据</h2>
     */
    private String parseBodyFromRequest(ServerHttpRequest request){
        //获取请求体
        Flux<DataBuffer> body = request.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();

        //订阅缓冲区去消费请求体中的数据
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            //一定要使用DataBufferUtils.release方法释放缓冲区的数据，否则，内存泄露
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });

        return bodyRef.get();
    }
}
